AWS CLIがAWS WAFとALBの関連付けをサポートしていたので試してみた
はじめに
こんにちは、中山です。
最近のアップデートで発表されたAWS WAFのALB対応、心待ちにされていた方々も多かったと思います。AWS上にシステムを構築する上で構成パターンが大きく増える嬉しいアップデートでした。弊社でも以下のエントリでその内容をまとめています。
この発表とほぼ同等のタイミングでアップデートされたAWS CLIのバージョン1.11.28でもこの機能に対応しました。AWS CLIで直接この機能を設定するという機会は少ないかもしれませんが、私自身まだAWS WAFとALBの関連付けを試したことがなかったということもあり、お勉強として使ってみたので本エントリにまとめたいと思います。
なお、AWS CLIはその後も順調にアップデートし続けており、執筆時点(2016/12/18)の最新バージョンは1.11.30です。今回はこのバージョンを利用します。
概要
AWS CLIでAWS WAFとALBの関連付けをするためには aws waf-regional
というコマンドを利用します。現在提供されている各種サブコマンドの一覧は以下の通りです。コマンド名やその使い方は従来のコマンド、 aws waf
とほぼ同等ですが、ヘルプの文言や指定可能なオプションなどが微妙に異なっているようです。
- associate-web-acl
- create-byte-match-set
- create-ip-set
- create-rule
- create-size-constraint-set
- create-sql-injection-match-set
- create-web-acl
- create-xss-match-set
- delete-byte-match-set
- delete-ip-set
- delete-rule
- delete-size-constraint-set
- delete-sql-injection-match-set
- delete-web-acl
- delete-xss-match-set
- disassociate-web-acl
- get-byte-match-set
- get-change-token
- get-change-token-status
- get-ip-set
- get-rule
- get-sampled-requests
- get-size-constraint-set
- get-sql-injection-match-set
- get-web-acl
- get-web-acl-for-resource
- get-xss-match-set
- list-byte-match-sets
- list-ip-sets
- list-resources-for-web-acl
- list-rules
- list-size-constraint-sets
- list-sql-injection-match-sets
- list-web-acls
- list-xss-match-sets
- update-byte-match-set
- update-ip-set
- update-rule
- update-size-constraint-set
- update-sql-injection-match-set
- update-web-acl
- update-xss-match-set
本エントリでは以下の構成図のようにすでにALBやそのロードバランシング先であるEC2インスタンスが作成済みの構成に、AWS WAFをAWS CLIで作成し、ALBとひも付けるという形で進めていきたいと思います。
なお、AWS WAFはchange tokenという一時的なトークンを指定しAPIのコンフリクトを防ぐという仕様になっています。トークンは aws waf-regional get-change-token
で取得、 aws waf-regional get-change-token-status
で状態の確認が可能です。トークンの状態は以下のように遷移していきます。
PROVISIONED
- 新規でトークンを取得した際の状態
- このトークンを使用して各種API操作をする
PENDING
- APIの操作にトークンを指定した直後の状態
- AWS WAFが各種WAFサーバに変更内容を広報する
INSYNC
- 変更内容が各種WAFサーバに広報された状態
- この状態のトークンは利用不可
各種APIの操作時に毎回トークンの取得及びオプションで指定という作業をする必要があるので、以下のようなシェルスクリプトを用意しておき(今回は token.sh
という名前にします)、 --change-token
オプションに指定すると便利です。
#!/usr/bin/env bash aws waf-regional get-change-token \ --query 'ChangeToken' \ --output text
また、コマンドで指定する引数にはオプションで直接設定する方法と、設定が記述されたJSONファイルを --cli-input-json
で指定する2つの方法が用意されています。今回はオプションで指定する方法で統一します。JSONファイルを利用する方法については以下のエントリを参照してください。
IPマッチコンディションの作成
まずはIPアドレスベースの制限をするためにIPマッチコンディションを作成します。
# 作成 $ aws waf-regional create-ip-set \ --name test-ip-set \ --change-token $(./token.sh) { "IPSet": { "IPSetId": "907fe999-ff29-4d5b-801f-fa2c1636ec88", "Name": "test-ip-set", "IPSetDescriptors": [] }, "ChangeToken": "57766fbf-8086-4488-91e2-0b4e5397b175" } # 確認 $ aws waf-regional list-ip-sets { "IPSets": [ { "IPSetId": "907fe999-ff29-4d5b-801f-fa2c1636ec88", "Name": "test-ip-set" } ], "NextMarker": "907fe999-ff29-4d5b-801f-fa2c1636ec88" } # アップデート $ aws waf-regional update-ip-set \ --ip-set-id "907fe999-ff29-4d5b-801f-fa2c1636ec88" \ --change-token $(./token.sh) \ --updates 'Action=INSERT,IPSetDescriptor={Type=IPV4,Value=******************}' { "ChangeToken": "4a3f7677-4cdc-4767-a75b-f15eb9c914be" } # 設定内容の確認 $ aws waf-regional get-ip-set \ --ip-set-id "907fe999-ff29-4d5b-801f-fa2c1636ec88" { "IPSet": { "IPSetId": "907fe999-ff29-4d5b-801f-fa2c1636ec88", "Name": "test-ip-set", "IPSetDescriptors": [ { "Type": "IPV4", "Value": "******************" } ] } }
文字列マッチコンディションの作成
続いて文字列ベースの制限をするために文字列マッチコンディションを作成します。
# 作成 $ aws waf-regional create-byte-match-set \ --name test-byte-match-set \ --change-token $(./token.sh) { "ChangeToken": "defab065-ebac-46f2-86d4-94d7710dd325", "ByteMatchSet": { "ByteMatchSetId": "2baae561-4af1-4ee3-8763-aef4deb8056c", "Name": "test-byte-match-set", "ByteMatchTuples": [] } } # 確認 $ aws waf-regional list-byte-match-sets { "NextMarker": "65d0a28c-6026-4cab-b241-d6bb6f9a4346", "ByteMatchSets": [ { "ByteMatchSetId": "2baae561-4af1-4ee3-8763-aef4deb8056c", "Name": "test-byte-match-set" } ] } # アップデート $ aws waf-regional update-byte-match-set \ --byte-match-set-id "2baae561-4af1-4ee3-8763-aef4deb8056c" \ --updates 'Action=INSERT,ByteMatchTuple={FieldToMatch={Type=HEADER,Data=user-agent},TargetString=test-string,TextTransformation=LOWERCASE,PositionalConstraint=CONTAINS}' \ --change-token $(./token.sh) { "ChangeToken": "f9e4ee8b-ebac-4f9d-9f1f-fde97b404017" } # 設定内容の確認 $ aws waf-regional get-byte-match-set \ --byte-match-set-id "2baae561-4af1-4ee3-8763-aef4deb8056c" { "ByteMatchSet": { "ByteMatchSetId": "2baae561-4af1-4ee3-8763-aef4deb8056c", "Name": "test-byte-match-set", "ByteMatchTuples": [ { "TargetString": "dGVzdC1zdHJpbmc=", "PositionalConstraint": "CONTAINS", "TextTransformation": "LOWERCASE", "FieldToMatch": { "Data": "user-agent", "Type": "HEADER" } } ] } }
サイズ制限コンディションの作成
次はリクエストサイズベースの制限をするためにサイズ制限コンディションを作成します。
# 作成 $ aws waf-regional create-size-constraint-set \ --name test-size-constraint-set \ --change-token $(./token.sh) { "SizeConstraintSet": { "SizeConstraints": [], "SizeConstraintSetId": "9a8e9491-39f7-47d5-a457-fb2f49242472", "Name": "test-size-constraint-set" }, "ChangeToken": "62cc3ea2-2a65-4fe9-9063-7474583cf497" } # 確認 $ aws waf-regional list-size-constraint-sets { "NextMarker": "9a8e9491-39f7-47d5-a457-fb2f49242472", "SizeConstraintSets": [ { "SizeConstraintSetId": "9a8e9491-39f7-47d5-a457-fb2f49242472", "Name": "test-size-constraint-set" } ] } # アップデート $ aws waf-regional update-size-constraint-set \ --size-constraint-set-id "9a8e9491-39f7-47d5-a457-fb2f49242472" \ --updates 'Action=INSERT,SizeConstraint={FieldToMatch={Type=HEADER,Data=user-agent},TextTransformation=NONE,ComparisonOperator=GT,Size=15}' \ --change-token $(./token.sh) { "ChangeToken": "2bc37308-370f-4b0c-88e0-19797c3e7cb2" } # 設定内容の確認 $ aws waf-regional get-size-constraint-set \ --size-constraint-set-id "9a8e9491-39f7-47d5-a457-fb2f49242472" { "SizeConstraintSet": { "SizeConstraints": [ { "ComparisonOperator": "GT", "TextTransformation": "NONE", "FieldToMatch": { "Data": "user-agent", "Type": "HEADER" }, "Size": 15 } ], "SizeConstraintSetId": "9a8e9491-39f7-47d5-a457-fb2f49242472", "Name": "test-size-constraint-set" } }
XSSマッチコンディションの作成
今度はXSSベースの制限をするためにXSSマッチコンディションを作成します。
# 作成 $ aws waf-regional create-xss-match-set \ --name test-xss-match-set \ --change-token $(./token.sh) { "ChangeToken": "8f3fc459-dc4b-4f6b-9291-cdc68efc669a", "XssMatchSet": { "XssMatchTuples": [], "XssMatchSetId": "f78cda06-4d1f-455c-ba9b-4506efe5f765", "Name": "test-xss-match-set" } } # 確認 $ aws waf-regional list-xss-match-sets { "XssMatchSets": [ { "XssMatchSetId": "f78cda06-4d1f-455c-ba9b-4506efe5f765", "Name": "test-xss-match-set" } ], "NextMarker": "f78cda06-4d1f-455c-ba9b-4506efe5f765" } # アップデート $ aws waf-regional update-xss-match-set \ --xss-match-set-id "f78cda06-4d1f-455c-ba9b-4506efe5f765" \ --updates 'Action=INSERT,XssMatchTuple={FieldToMatch={Type=QUERY_STRING,Data=string},TextTransformation=NONE}' \ --change-token $(./token.sh) { "ChangeToken": "8821ff28-68d7-453e-bfe1-f88b2bf6ceab" } # 設定内容の確認 $ aws waf-regional get-xss-match-set \ --xss-match-set-id "f78cda06-4d1f-455c-ba9b-4506efe5f765" { "XssMatchSet": { "XssMatchTuples": [ { "TextTransformation": "NONE", "FieldToMatch": { "Type": "QUERY_STRING" } } ], "XssMatchSetId": "f78cda06-4d1f-455c-ba9b-4506efe5f765", "Name": "test-xss-match-set" } }
SQLiマッチコンディションの作成
最後にSQLiベースの制限をするためにSQLiマッチコンディションを作成します。
# 作成 $ aws waf-regional create-sql-injection-match-set \ --name test-sql-injection-match-set \ --change-token $(./token.sh) { "ChangeToken": "9ef13dbb-0e96-4e13-973a-df2ac7720518", "SqlInjectionMatchSet": { "SqlInjectionMatchTuples": [], "Name": "test-sql-injection-match-set", "SqlInjectionMatchSetId": "81c823ed-40e8-4af3-ba49-d061bdda0374" } } # 確認 $ aws waf-regional list-sql-injection-match-sets { "SqlInjectionMatchSets": [ { "Name": "test-sql-injection-match-set", "SqlInjectionMatchSetId": "81c823ed-40e8-4af3-ba49-d061bdda0374" } ], "NextMarker": "81c823ed-40e8-4af3-ba49-d061bdda0374" } # アップデート $ aws waf-regional update-sql-injection-match-set \ --sql-injection-match-set-id "81c823ed-40e8-4af3-ba49-d061bdda0374" \ --updates 'Action=INSERT,SqlInjectionMatchTuple={FieldToMatch={Type=QUERY_STRING,Data=string},TextTransformation=URL_DECODE}' \ --change-token $(./token.sh) { "ChangeToken": "fe213af4-48ea-46af-8047-fd608922bec2" } # 設定内容の確認 $ aws waf-regional get-sql-injection-match-set \ --sql-injection-match-set-id "81c823ed-40e8-4af3-ba49-d061bdda0374" { "SqlInjectionMatchSet": { "SqlInjectionMatchTuples": [ { "TextTransformation": "URL_DECODE", "FieldToMatch": { "Type": "QUERY_STRING" } } ], "Name": "test-sql-injection-match-set", "SqlInjectionMatchSetId": "81c823ed-40e8-4af3-ba49-d061bdda0374" } }
ルール及びWeb ACLの作成
今回は説明を簡易的にするために、先ほど作成した各種コンディションを1つのルールに関連付け、そのルールをWeb ACLに設定する方式にします。ただしおハマりポイントがあります。というかエントリをまとめていく時点で私もハマりました。。。 AWS WAFはルールに関連付けられた各種コンディションをANDとして評価する という点です。ANDになってしまうと全てのコンディションにマッチした場合にアクションが実行されるという動作になってしまいます。今回は各コンディションの動作を一つ一つ確認したいのでANDではなくORにする必要があります。幸い、弊社森永がこの点について詳細にエントリをまとめてくれているのでそれを参考にして設定していきます。
内容をまとめると以下のように設定すればOKです。
- ルールに関連付ける各種コンディションを否定する
Negated
をtrue
にする- Web ACLに関連付けるルールを許可する
Action
をALLOW
にする- Web ACLのデフォルトアクションをブロックにする
--default-action
でType=BLOCK
を指定
おハマりポイントを理解したところで、早速ルールを作成していきましょう。
# 作成 $ aws waf-regional create-rule \ --name test-rule \ --metric-name testRule \ --change-token $(./token.sh) { "ChangeToken": "d423d031-3d6d-45f8-a29f-f3e82bf36b9a", "Rule": { "Predicates": [], "MetricName": "testRule", "Name": "test-rule", "RuleId": "80b267df-5711-4e61-8b6a-d7970376ec9f" } } # 確認 $ aws waf-regional list-rules { "Rules": [ { "Name": "test-rule", "RuleId": "80b267df-5711-4e61-8b6a-d7970376ec9f" } ], "NextMarker": "80b267df-5711-4e61-8b6a-d7970376ec9f" } # IPマッチコンディションの関連付け $ aws waf-regional update-rule \ --rule-id "80b267df-5711-4e61-8b6a-d7970376ec9f" \ --updates 'Action=INSERT,Predicate={Negated=true,Type=IPMatch,DataId=907fe999-ff29-4d5b-801f-fa2c1636ec88}' \ --change-token $(./token.sh) { "ChangeToken": "681a8108-cdba-4f7d-9a19-6c56f372e633" } # 文字列マッチコンディションの関連付け $ aws waf-regional update-rule \ --rule-id "80b267df-5711-4e61-8b6a-d7970376ec9f" \ --updates 'Action=INSERT,Predicate={Negated=true,Type=ByteMatch,DataId=2baae561-4af1-4ee3-8763-aef4deb8056c}' \ --change-token $(./token.sh) { "ChangeToken": "6cd82c71-6e35-48d4-9206-16a78e68c2de" } # サイズ制限コンディションの関連付け $ aws waf-regional update-rule \ --rule-id "80b267df-5711-4e61-8b6a-d7970376ec9f" \ --updates 'Action=INSERT,Predicate={Negated=true,Type=SizeConstraint,DataId=9a8e9491-39f7-47d5-a457-fb2f49242472}' \ --change-token $(./token.sh) { "ChangeToken": "66bb2e5b-7268-483e-b8ec-6b9133c22e9d" } # XSSマッチコンディションの関連付け $ aws waf-regional update-rule \ --rule-id "80b267df-5711-4e61-8b6a-d7970376ec9f" \ --updates 'Action=INSERT,Predicate={Negated=true,Type=XssMatch,DataId=f78cda06-4d1f-455c-ba9b-4506efe5f765}' \ --change-token $(./token.sh) { "ChangeToken": "3bd62a93-72bd-4832-9e54-7408b7f2c55d" } # SQLiマッチコンディションの関連付け $ aws waf-regional update-rule \ --rule-id "80b267df-5711-4e61-8b6a-d7970376ec9f" \ --updates 'Action=INSERT,Predicate={Negated=true,Type=SqlInjectionMatch,DataId=81c823ed-40e8-4af3-ba49-d061bdda0374}' \ --change-token $(./token.sh) { "ChangeToken": "41d5bdb0-f706-4c78-8cc7-490ef5a8cfa3" } # 設定内容の確認 $ aws waf-regional get-rule \ --rule-id "80b267df-5711-4e61-8b6a-d7970376ec9f" { "Rule": { "Predicates": [ { "Negated": true, "Type": "SizeConstraint", "DataId": "9a8e9491-39f7-47d5-a457-fb2f49242472" }, { "Negated": true, "Type": "SqlInjectionMatch", "DataId": "81c823ed-40e8-4af3-ba49-d061bdda0374" }, { "Negated": true, "Type": "IPMatch", "DataId": "907fe999-ff29-4d5b-801f-fa2c1636ec88" }, { "Negated": true, "Type": "XssMatch", "DataId": "f78cda06-4d1f-455c-ba9b-4506efe5f765" }, { "Negated": true, "Type": "ByteMatch", "DataId": "2baae561-4af1-4ee3-8763-aef4deb8056c" } ], "MetricName": "testRule", "Name": "test-rule", "RuleId": "80b267df-5711-4e61-8b6a-d7970376ec9f" } }
続いてWeb ACLを作成します。
# 作成 $ aws waf-regional create-web-acl \ --name test-web-acl \ --metric-name testWebACL \ --default-action 'Type=BLOCK' \ --change-token $(./token.sh) { "WebACL": { "DefaultAction": { "Type": "BLOCK" }, "Rules": [], "MetricName": "testWebACL", "WebACLId": "551f6c8e-e272-4063-b26f-a49199aa51f7", "Name": "test-web-acl" }, "ChangeToken": "7f857b47-b81a-4f4f-a455-82204fb08a46" } # 確認 $ aws waf-regional list-web-acls { "NextMarker": "551f6c8e-e272-4063-b26f-a49199aa51f7", "WebACLs": [ { "WebACLId": "551f6c8e-e272-4063-b26f-a49199aa51f7", "Name": "test-web-acl" } ] } # ルールの関連付け $ aws waf-regional update-web-acl \ --web-acl-id "551f6c8e-e272-4063-b26f-a49199aa51f7" \ --updates 'Action=INSERT,ActivatedRule={Priority=1,RuleId=80b267df-5711-4e61-8b6a-d7970376ec9f,Action={Type=ALLOW}}' \ --change-token $(./token.sh) { "ChangeToken": "084d628e-f247-47c2-8531-441c4cae0d7e" } # 設定内容の確認 $ aws waf-regional get-web-acl \ --web-acl-id "551f6c8e-e272-4063-b26f-a49199aa51f7" { "WebACL": { "DefaultAction": { "Type": "BLOCK" }, "Rules": [ { "Priority": 1, "Action": { "Type": "ALLOW" }, "RuleId": "80b267df-5711-4e61-8b6a-d7970376ec9f" } ], "MetricName": "testWebACL", "WebACLId": "551f6c8e-e272-4063-b26f-a49199aa51f7", "Name": "test-web-acl" } }
AWS WAFとALBとの関連付け
いよいよ最後の設定です。作成したWeb ACLとALBを関連付けます。
# AWS WAFとALBを関連付ける $ aws waf-regional associate-web-acl \ --web-acl-id "551f6c8e-e272-4063-b26f-a49199aa51f7" \ --resource-arn "arn:aws:elasticloadbalancing:ap-northeast-1:************:loadbalancer/app/test-alb/7b6ce71be4bf2f3a" # Web ACLがALBに関連付けされていることを確認 $ aws waf-regional list-resources-for-web-acl \ --web-acl-id "551f6c8e-e272-4063-b26f-a49199aa51f7" { "ResourceArns": [ "arn:aws:elasticloadbalancing:ap-northeast-1:************:loadbalancer/app/test-alb/7b6ce71be4bf2f3a" ] } # ALBからもWeb ACLと関連付けされていることを確認 $ aws waf-regional get-web-acl-for-resource \ --resource-arn "arn:aws:elasticloadbalancing:ap-northeast-1:************:loadbalancer/app/test-alb/7b6ce71be4bf2f3a" { "WebACLSummary": { "WebACLId": "551f6c8e-e272-4063-b26f-a49199aa51f7", "Name": "test-web-acl" } }
動作確認
最後に意図した動作をするか確認します。正常にアクセスできた場合はALBのロードバランシング先にある2つのEC2インスタンスの内、どちらかのWebページがレスポンスとして返されます。index.htmlにEC2インスタンスのホスト名を書き込んでいるので、その内容が返却されたらアクセス成功と判断します。
IPマッチコンディション
まずブロックされたIPアドレスからアクセスしてみます。
$ curl test-alb-1629991441.ap-northeast-1.elb.amazonaws.com <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>awselb/2.0</center> </body> </html>
ブロックされました。続いて別のIPアドレスからアクセスしてみます。
$ curl test-alb-1629991441.ap-northeast-1.elb.amazonaws.com ip-172-16-1-65
正常にアクセスできたようです。
文字列マッチコンディション
今回の設定では評価前に該当の文字列を小文字に変換した上で、ユーザエージェントに「test-string」という文字列が含まれていた場合にブロックするというものでした。まずはブロックさせてみます。
$ curl -H 'User-Agent: TEST-STRING' test-alb-1629991441.ap-northeast-1.elb.amazonaws.com <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>awselb/2.0</center> </body> </html>
続いて別の文字列をユーザエージェントに指定した上でアクセスしてみます。
$ curl -H 'User-Agent: TEST-ANOTHER-STING' test-alb-1629991441.ap-northeast-1.elb.amazonaws.com ip-172-16-2-17
正常にアクセスできました。
サイズ制限コンディション
今回の設定はユーザエージェントに15bytes以上の文字列が含まれていた場合にブロックするというものです。条件にマッチする形でアクセスしてみます。
$ curl -H 'User-Agent: abcdefghijklmnopqrstuvwxyz' test-alb-1629991441.ap-northeast-1.elb.amazonaws.com <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>awselb/2.0</center> </body> </html>
ブロックされました。次に15bytes未満でアクセスしてみます。
$ curl -H 'User-Agent: abc' test-alb-1629991441.ap-northeast-1.elb.amazonaws.com ip-172-16-1-65
アクセス成功です。
XSSマッチコンディション
今回の設定はクエリにXSSと疑われる文字列が指定されていた場合にブロックするというものでした。簡易的なXSSっぽい文字列を指定してみます。
$ curl 'test-alb-1629991441.ap-northeast-1.elb.amazonaws.com/?<SCRIPT>alert(Cookie”+document.cookie)</SCRIPT>' <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>awselb/2.0</center> </body> </html>
ブロックされました。続いて普通のクエリを指定してみます。
$ curl 'test-alb-1629991441.ap-northeast-1.elb.amazonaws.com/?aaa=bbb' ip-172-16-2-17
正常にアクセス可能ですね。
SQLiマッチコンディション
今回の設定は評価前にクエリに渡された文字列をURLデコードし、不正なSQLが検出された場合にブロックするというものです。簡易的にSQLっぽい文字列を指定してアクセスしてみます。
$ curl 'test-alb-1629991441.ap-northeast-1.elb.amazonaws.com/index.html?id=1%20UNION%20ALL%20SELECT%20NULL--%20' <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>awselb/2.0</center> </body> </html>
ブロックされました。続いて普通のクエリを指定してみます。
$ curl 'test-alb-1629991441.ap-northeast-1.elb.amazonaws.com/index.html?id=1' ip-172-16-1-65
アクセス可能ですね。
まとめ
いかがだったでしょうか。
AWS WAFとALBの関連付けをAWS CLIで設定してみました。やはりこういった作業はマネジメントコンソールでないとちょっとシンドいですね。。。ただし、AWS CLIで設定する場合はかなり低レイヤな部分を自ら指定する必要があるので、マネジメントコンソールではいい感じに隠蔽されている各種概念など思わぬ発見もあり、お勉強用途には良いかと思います。お時間がある時に「あえて」AWS CLIを利用するのも良いのではないでしょうか。
本エントリがみなさんの参考になったら幸いに思います。